feat(web): mermaid diagram lightbox on click#741
Conversation
There was a problem hiding this comment.
Findings
- No issues found.
Summary
Review mode: initial
Reviewed the Mermaid lightbox and new ZoomableLightbox diff in full. No blocker/major/minor issues met the confidence threshold. Residual risk: mobile gesture behavior and focus behavior were assessed statically only.
Testing
- Not run (automation):
bunis not installed in this runner, so targeted web tests could not be executed.
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Fit from unscaled dimensions —
applyFitScale()can run once while the loading placeholder is scaled up, then re-run whenfitContentKeychanges after the SVG arrives. BecausemeasureContentSize()readsgetBoundingClientRect()from an element inside the already-transformed content wrapper, the second measurement includes the previous scale and computes a much smaller fit scale. Large diagrams can open tiny instead of fitted after the async Mermaid render completes. Evidence:web/src/components/ZoomableLightbox.tsx:39.
Suggested fix:function measureContentSize(content: HTMLElement, scale = 1): { width: number; height: number } | null { const safeScale = scale > 0 ? scale : 1 const svg = content.querySelector("svg") if (svg) { const box = svg.getBoundingClientRect() if (box.width > 0 && box.height > 0) { return { width: box.width / safeScale, height: box.height / safeScale } } } const rect = content.getBoundingClientRect() if (rect.width > 0 && rect.height > 0) { return { width: rect.width / safeScale, height: rect.height / safeScale } } return null } const contentSize = measureContentSize(content, scaleRef.current)
Questions
- None.
Summary
Review mode: follow-up after new commits
Found one fit regression in the updated lightbox sizing path. The prior initial review had no findings; this issue is from the follow-up fitting changes.
Testing
- Not run (automation)
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Fit retries measure the already-scaled SVG —
measureSvgIntrinsicSize()readsgetBoundingClientRect()before intrinsic sources, and the 50ms/200ms retries callapplyFitScale()again after the content wrapper has a CSSscale(...). Because ancestor transforms are included in the rect, the retry recomputes the fit from scaled dimensions and moves the viewer away from its fitted size. Large diagrams can jump oversized; small diagrams can shrink back toward 100%. Evidence:web/src/components/ZoomableLightbox.tsx:37.
Suggested fix:function measureSvgIntrinsicSize(svg: SVGSVGElement, currentScale = 1): { width: number; height: number } | null { const viewBox = svg.viewBox?.baseVal if (viewBox && viewBox.width > 0 && viewBox.height > 0) { return { width: viewBox.width, height: viewBox.height } } const widthAttr = Number.parseFloat(svg.getAttribute('width') ?? '') const heightAttr = Number.parseFloat(svg.getAttribute('height') ?? '') if (widthAttr > 0 && heightAttr > 0) { return { width: widthAttr, height: heightAttr } } const safeScale = currentScale > 0 ? currentScale : 1 const box = svg.getBoundingClientRect() if (box.width > 0 && box.height > 0) { return { width: box.width / safeScale, height: box.height / safeScale } } return null } const contentSize = measureContentSize(content, scaleRef.current)
Questions
- None.
Summary
Review mode: follow-up after new commits
The previous sizing issue is still present in the updated implementation: the code added intrinsic fallbacks, but the transformed getBoundingClientRect() path still wins before those fallbacks and the scheduled retries can overwrite the fitted scale.
Testing
- Not run (automation:
bunis not installed in this runner)
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Fit retries measure the already-scaled SVG, so the lightbox can jump away from its fitted size after opening.
measureSvgIntrinsicSize()still returnsgetBoundingClientRect()before intrinsic sources, and the 50ms/200ms retries callapplyFitScale()again after the content wrapper hasscale(...); ancestor transforms are included in that rect. Evidence:web/src/components/ZoomableLightbox.tsx:37.
Suggested fix:function measureSvgIntrinsicSize(svg: SVGSVGElement, currentScale = 1): { width: number; height: number } | null { const viewBox = svg.viewBox?.baseVal if (viewBox && viewBox.width > 0 && viewBox.height > 0) { return { width: viewBox.width, height: viewBox.height } } const widthAttr = Number.parseFloat(svg.getAttribute('width') ?? '') const heightAttr = Number.parseFloat(svg.getAttribute('height') ?? '') if (widthAttr > 0 && heightAttr > 0) { return { width: widthAttr, height: heightAttr } } const safeScale = currentScale > 0 ? currentScale : 1 const box = svg.getBoundingClientRect() if (box.width > 0 && box.height > 0) { return { width: box.width / safeScale, height: box.height / safeScale } } return null } const contentSize = measureContentSize(content, scaleRef.current)
Questions
- None.
Summary
Review mode: follow-up after new commits
The previous sizing issue is still present in the latest head. The new SVG lightbox preparation improves the Mermaid markup, but ZoomableLightbox can still refit from transformed dimensions during its delayed retry pass.
Testing
- Not run (automation:
bunis not installed in this runner)
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Lightbox fit can still compound from the transformed image size. The follow-up moved the modal content to an
<img>, butmeasureContentSize()readsgetBoundingClientRect()beforenaturalWidth/naturalHeight; the scheduled 50ms/200ms refits run after the wrapper hasscale(...), so a diagram that initially fits at 50% can be remeasured at half size and bumped back toward 100%, overflowing the screen. Evidence:web/src/components/ZoomableLightbox.tsx:59.
Suggested fix:function measureContentSize(content: HTMLElement): { width: number; height: number } | null { const img = content.querySelector('img') if (img) { if (img.naturalWidth > 0 && img.naturalHeight > 0) { return { width: img.naturalWidth, height: img.naturalHeight } } const box = img.getBoundingClientRect() if (box.width > 0 && box.height > 0) { const currentScale = scaleRef.current > 0 ? scaleRef.current : 1 return { width: box.width / currentScale, height: box.height / currentScale } } } // keep existing SVG/content fallback handling }
Questions
- None.
Summary
Review mode: follow-up after new commits
The previous fit bug is still reproducible through the new image-based lightbox path because delayed retries measure transformed dimensions. This affects the main user-facing behavior of opening large Mermaid diagrams on smaller screens.
Testing
- Not run:
bunis not installed in this runner.
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Lightbox fit can still compound from transformed SVG measurements.
measureSvgIntrinsicSize()readsgetBoundingClientRect()before the SVGviewBox/attribute dimensions, but the SVG is inside the element that already hasscale(...)applied. When the lightbox first fits the loading placeholder and then refits afterlightboxSvgarrives, the 50ms/200ms retries can measure the already-scaled SVG and change the fit again, so large diagrams can jump too small or back toward viewport overflow. Evidence:web/src/components/ZoomableLightbox.tsx:37.
Suggested fix:function measureSvgIntrinsicSize(svg: SVGSVGElement): { width: number; height: number } | null { const viewBox = svg.viewBox?.baseVal if (viewBox && viewBox.width > 0 && viewBox.height > 0) { return { width: viewBox.width, height: viewBox.height } } const widthAttr = Number.parseFloat(svg.getAttribute("width") ?? "") const heightAttr = Number.parseFloat(svg.getAttribute("height") ?? "") if (widthAttr > 0 && heightAttr > 0) { return { width: widthAttr, height: heightAttr } } const box = svg.getBoundingClientRect() if (box.width > 0 && box.height > 0) { return { width: box.width, height: box.height } } return null }
Questions
- None.
Summary
Review mode: follow-up after new commits
The previous image-specific path was removed, but the current direct-SVG lightbox still has a transformed-measurement feedback loop in the shared fit logic. This affects the primary behavior of opening dense Mermaid diagrams and fitting them to small screens.
Testing
- Not run (automation; review-only, did not execute PR code).
HAPI Bot
There was a problem hiding this comment.
Findings
- No high-confidence issues found in the added/modified lines.
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The previous lightbox sizing concern appears addressed for the Mermaid path by passing intrinsic viewBox dimensions into
ZoomableLightbox. Residual risk: I could not execute the web test suite in this runner becausebunis unavailable.
Testing
- Not run:
bunnot found in environment when attempting the PR-listed web tests.
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Fit-to-screen still clips very large diagrams —
applyFitScaleruns the computed fit ratio throughclampScale, whose minimum is0.25, so any diagram that needs a scale below 25% cannot actually fit on open or after pressing the fit/reset control. Dense Mermaid diagrams can easily exceed 4x the viewport, which is the case this lightbox is meant to solve. Evidenceweb/src/components/ZoomableLightbox.tsx:160.
Suggested fix:function clampScale(value: number, minScale = MIN_SCALE): number { return Math.min(MAX_SCALE, Math.max(minScale, value)) } const rawFitScale = Math.min(fitWidth, fitHeight) const fitScale = clampScale(rawFitScale, 0.01)
- [Minor] Playwright helper starts Vite from
scripts/web—REPO_ROOTresolves one directory up fromscripts/dev, which isscripts, soWEB_DIRbecomes<repo>/scripts/web. The new root script setsMERMAID_E2E_START_DEV=1, so this path prevents the helper from starting the actual web dev server. Evidencescripts/dev/mermaid-lightbox-playwright.mjs:18.
Suggested fix:const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..') const WEB_DIR = resolve(REPO_ROOT, 'web')
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The follow-up improves Mermaid lightbox sizing coverage, but the fit logic still has a minimum-scale floor that breaks true fit-to-screen for very large diagrams, and the new Playwright script cannot start the web app from the correct directory.
Testing
- Not run (automation):
bunis not installed and dependencies are not present in this runner.
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Fit-to-screen still clips very large diagrams —
applyFitScalefloors the computed fit ratio at0.25, so any diagram needing less than 25% scale cannot fit on open or after reset. Evidenceweb/src/components/ZoomableLightbox.tsx:160.
Suggested fix:function clampScale(value: number, minScale = MIN_SCALE): number { return Math.min(MAX_SCALE, Math.max(minScale, value)) } const fitScale = clampScale(Math.min(fitWidth, fitHeight), 0.01)
- [Minor] Playwright artifacts are not ignored —
web/.gitignoreis evaluated from theweb/directory, soweb/test-results/ignoresweb/web/test-results/while Playwright writesweb/test-results/. Evidenceweb/.gitignore:4.
Suggested fix:test-results/
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The previous helper path issue was addressed by moving server lifecycle into Playwright config, but the large-diagram fit bug remains. The new E2E artifact ignore pattern also misses the actual generated directory.
Testing
- Not run (automation)
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Interactive zoom still snaps huge diagrams back to 25% —
MIN_FIT_SCALElets the lightbox open dense diagrams below the normal 0.25 floor, but wheel/pinch zoom still callclampScale(...)without that lower fit floor. Starting from a fitted scale like0.05, a pinch gesture or wheel-down jumps back to0.25, clipping the same large diagrams the fit path just fixed. Evidenceweb/src/components/ZoomableLightbox.tsx:186.
Suggested fix:const zoomBy = useCallback((delta: number) => { const minScale = Math.min(MIN_SCALE, baseScaleRef.current) updateScale((current) => clampScale(current + delta, minScale)) }, [updateScale]) const nextScale = pinch.startDistance > 0 ? clampScale( pinch.startScale * (distance / pinch.startDistance), Math.min(MIN_SCALE, baseScaleRef.current), ) : pinch.startScale
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The previous two bot findings are addressed, but the new fit floor is not carried into interactive zoom, so very large diagrams can still become clipped as soon as the user zooms/pinches.
Testing
- Not run:
bunis not installed in this runner (bun: command not found).
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Interactive zoom still snaps large fitted diagrams back to 25% —
MIN_FIT_SCALElets the lightbox initially fit dense diagrams below the normal0.25floor, but wheel, keyboard, button, and pinch zoom still callclampScale(...)with the default floor. A diagram opened at0.05will jump/clamp back to0.25on zoom-out or pinch, reintroducing clipping for the same large diagrams the fit path is meant to handle. Evidenceweb/src/components/ZoomableLightbox.tsx:186.
Suggested fix:const getMinInteractiveScale = () => Math.min(MIN_SCALE, baseScaleRef.current) const zoomBy = useCallback((delta: number) => { updateScale((current) => clampScale(current + delta, getMinInteractiveScale())) }, [updateScale]) const nextScale = pinch.startDistance > 0 ? clampScale( pinch.startScale * (distance / pinch.startDistance), getMinInteractiveScale(), ) : pinch.startScale
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The current head still leaves the under-25% fit scale out of the interactive zoom paths, so large diagrams can become clipped immediately after user interaction. No other high-confidence issues found in the changed lines.
Testing
- Not run (automation):
bunis not installed in this runner.
HAPI Bot
…zoom Resolves PR tiann#741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com>
6e9cb0a to
aa6f755
Compare
|
Rebased onto current Fit-scale measurement (Major × ~7 threads, all on Interactive zoom floor (Major × 2 threads) Coverage The two |
There was a problem hiding this comment.
Findings
- [Major] Seed script can wipe a real session's messages —
SESSION_IDis documented as configurable, and this script is meant to run against the host's real~/.hapi/hapi.db; if that env var is set to an existing non-fixture session, line 57 deletes every message in it before inserting the mermaid cases. Guard the destructive delete so only the known test fixture session/tag can be replaced. Evidencescripts/dev/mermaid-lightbox-seed-session-db.ts:57.
Suggested fix:const existing = db.prepare('SELECT id, tag FROM sessions WHERE id = ?').get(sessionId) as { id: string tag: string | null } | undefined if (existing && existing.tag !== sessionTag) { throw new Error(`Refusing to replace messages for non-${sessionTag} session ${sessionId}`) }
Questions
- None.
Summary
- Review mode: follow-up after new commits
- Found one data-loss risk in the live dogfood seed helper. The prior interactive zoom floor issue appears addressed in the current diff.
Testing
- Not run:
bunis not installed in this runner, sobun run typecheck:webcould not execute.
HAPI Bot
HAPI Bot Major (PR tiann#741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
- None.
Questions
- None.
Summary
- Review mode: follow-up after new commits
- No issues found in the latest PR diff. The prior seed-script data-loss finding appears addressed by the fixture tag guard in
scripts/dev/mermaid-lightbox-seed-session-db.ts:42.
Testing
- Not run (automation):
bunis not installed in this runner, sobun run typecheck:webcould not execute. Local Playwright commands also could not run because the workspace dependencies/binaries are not installed here.
HAPI Bot
There was a problem hiding this comment.
Findings
- None.
Questions
- None.
Summary
- Review mode: follow-up after new commits
- No issues found in the latest PR diff. Residual risk is mainly browser/device coverage for touch gestures and SVG sizing across Mermaid diagram types; the PR adds Vitest plus Playwright coverage for those paths.
Testing
- Not run (automation): workspace dependencies are not installed in this runner, and
bunis unavailable here.
HAPI Bot
There was a problem hiding this comment.
Findings
- [Major] Guard
ResizeObserverbefore constructing it — the new lightbox effect runs when a Mermaid dialog opens, but the repo's Vitest jsdom setup does not polyfillResizeObserver, so the focused click path can throw before the dialog renders. This also makes the shared component brittle in browsers/webviews without the API. Evidenceweb/src/components/ZoomableLightbox.tsx:362.
Suggested fix:if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', apply) return () => window.removeEventListener('resize', apply) } const resize = new ResizeObserver(apply) resize.observe(toolbar) window.addEventListener('resize', apply) return () => { resize.disconnect() window.removeEventListener('resize', apply) }
Questions
- None.
Summary
Review mode: follow-up after new commits
One major issue found in the latest head: the shared lightbox assumes ResizeObserver exists during open, which can break the new Mermaid lightbox tests/runtime path.
Testing
- Not run (automation):
bunis unavailable in this runner (/bin/bash: bun: command not found).
HAPI Bot
HAPI Bot Major (PR tiann#741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com>
Click rendered mermaid blocks in chat to open a zoomable full-screen viewer. Re-renders from source in the modal with the current theme. Closes tiann#737. Co-authored-by: Cursor <cursoragent@cursor.com>
Auto-scale diagrams to fill the viewer instead of opening at intrinsic mermaid size. Reset returns to fit; zoom label is relative to fit (100%). Co-authored-by: Cursor <cursoragent@cursor.com>
Use visualViewport for fit scale, full-screen pan layer, and a floating toolbar so the diagram can use the whole display. Co-authored-by: Cursor <cursoragent@cursor.com>
Second mermaid.render on open often left a 0×0 SVG while fit scale was computed from the loading placeholder. Reuse the inline SVG in the modal and measure viewBox with retried fit-to-screen. Co-authored-by: Cursor <cursoragent@cursor.com>
Inlining the same mermaid markup twice duplicates element ids and breaks url(#ref) resolution in the modal copy. Prefix ids and hrefs for lightbox only. Co-authored-by: Cursor <cursoragent@cursor.com>
Mermaid emits width="100%" with max-width in px; that collapses to 0×0 inside the centered lightbox layer. Derive width/height from viewBox for the uniquified lightbox clone. Co-authored-by: Cursor <cursoragent@cursor.com>
String id rewrites broke mermaid's embedded CSS so only labels appeared zoomed. Rasterize the inline SVG to a data-URL img instead of duplicating markup in the DOM. Co-authored-by: Cursor <cursoragent@cursor.com>
Data-URL images drop or blank some mermaid diagram types (sequence). Re-render with a modal-specific id into inline SVG on a code-bg panel, and add sequence theme variables for dark/light. Co-authored-by: Cursor <cursoragent@cursor.com>
Reuse the inline render in an isolated shadow root so sequence CSS stays intact, and fit the viewport from viewBox dimensions instead of the loading placeholder or width="100%" layout. Co-authored-by: Cursor <cursoragent@cursor.com>
Add e2e harness and a script that opens the lightbox for each diagram kind (flowchart through kanban). Fit uses inline getBBox() so compact charts like gitGraph fill the viewport. Co-authored-by: Cursor <cursoragent@cursor.com>
Playwright owns Vite lifecycle (no agent-spawned dev server). Fit uses viewBox unless viewBox padding is excessive (gitGraph); wide charts use width-based coverage in e2e. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Guard lightbox open when svg is null; allow fit scale down to 0.01 while keeping 0.25 minimum for manual zoom; ignore Playwright test-results/ correctly. Co-authored-by: Cursor <cursoragent@cursor.com>
Measure inline vs lightbox bounding box after click; require visible growth (area ratio or max dimension) plus dialog + shadow SVG content. Co-authored-by: Cursor <cursoragent@cursor.com>
Add seed script for a dedicated chat session, live hub Playwright suite (HAPI_LIVE=1), and dogfood doc. Live tests fail until driver serves shadow-DOM lightbox (catches gray-box regression on stale bundles). Co-authored-by: Cursor <cursoragent@cursor.com>
…zoom Resolves PR tiann#741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com>
HAPI Bot Major (PR tiann#741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com>
Mermaid emits width="100%" on every diagram. Inside a shadow root whose
host has no explicit size, that collapses to zero in Chromium for most
diagram types - only ones that ship pixel attrs (e.g. journey) happen to
render. Operator confirmed on the live driver: every diagram except
journey opened to a grey rounded square.
MermaidLightboxSvg now runs normalizeMermaidSvgForStandaloneDisplay before
injecting (strips width/height="100%", bakes viewBox dims as pixels) and
sets :host{display:inline-block} so the host sizes to the SVG. Inline svg
in chat is unchanged - only the lightbox copy is normalized.
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator screenshot showed the diagram top (e.g. pie 'Pets' title) clipped behind the toolbar bar. Two causes: 1. getScreenFitSize used the full viewport height, so the fit scale sized the diagram to fill an area the toolbar overlapped. 2. The viewport (drag/zoom area) was inset-0; content centered on the full viewport center, not the visible region's center, pushing the top behind the toolbar. Measure the toolbar with a ResizeObserver, subtract its height from the fit calculation (clamped at zero), and start the viewport region below the toolbar (top: toolbarHeight). Fit scale recomputes whenever toolbar height changes. Adds Vitest coverage for getScreenFitSize reserved-top math. Co-authored-by: Cursor <cursoragent@cursor.com>
HAPI Bot Major (PR tiann#741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com>
4584dfe to
b884b20
Compare
There was a problem hiding this comment.
Findings
- [Minor] Fix the live Playwright wrapper cwd —
node scripts/dev/mermaid-lightbox-live-playwright.mjsrunsnpm run test:mermaid-lightbox:livefromscripts/, but that directory has no package.json and no such script, so the wrapper exits before Playwright starts. Evidencescripts/dev/mermaid-lightbox-live-playwright.mjs:14.
Suggested fix:const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..') const result = spawnSync( npmBin, ['run', 'test:mermaid-lightbox:live'], { cwd: REPO_ROOT, stdio: 'inherit', env: { ...process.env, PATH: process.env.PATH, HAPI_LIVE: '1' }, }, )
Questions
- None.
Summary
Review mode: follow-up after new commits
One minor issue found in the latest head. The prior ResizeObserver blocker appears addressed in web/src/components/ZoomableLightbox.tsx. Residual risk: runtime/typecheck coverage could not be verified in this runner.
Testing
- Not run (automation):
bunis unavailable in this runner (/bin/bash: bun: command not found). git diff --checkpassed.
HAPI Bot
HAPI Bot Minor (PR tiann#741): the wrapper sets cwd to scripts/, but the test:mermaid-lightbox:live npm script lives in the repo-root package.json, so spawning npm there exited before Playwright started. Switch cwd to the repo root and drop the unused WEB_DIR constant. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
- [Minor] Accept signed SVG viewBox values before normalizing —
getMermaidSvgLayoutSizeonly matches digits, dots, and spaces, so a valid SVG viewBox such asviewBox="-8 -8 640 480"returnsnull. That makesnormalizeMermaidSvgForStandaloneDisplayreturn the original Mermaid SVG and keepwidth="100%", which can recreate the blank/zero-sized shadow-root lightbox this PR is fixing. Evidenceweb/src/components/assistant-ui/mermaid-diagram.tsx:98.
Suggested fix:export function getMermaidSvgLayoutSize(svg: string): { width: number; height: number } | null { const viewBoxMatch = svg.match(/\bviewBox=(['"])([^'"]+)\1/i) if (!viewBoxMatch) return null const parts = viewBoxMatch[2].trim().split(/[\s,]+/).map(Number) if (parts.length < 4 || parts.some(Number.isNaN) || parts[2] <= 0 || parts[3] <= 0) return null return { width: parts[2], height: parts[3] } }
Questions
- None.
Summary
Review mode: follow-up after new commits
The prior live Playwright wrapper cwd issue is fixed in the new commit. One minor issue remains in the SVG normalization path; it can miss valid Mermaid/SVG viewBoxes and leave the lightbox with the same relative sizing failure mode.
Testing
- Not run (automation):
bunand installed workspace dependencies are unavailable in this runner. git diff --checkpassed.
HAPI Bot
HAPI Bot Minor (PR tiann#741): the viewBox regex only matched digits, dots, and spaces, so a valid viewBox with negative origin (e.g. '-8 -8 640 480') returned null. normalizeMermaidSvgForStandaloneDisplay then became a no-op and left width='100%', re-introducing the zero-sized lightbox render this PR is meant to fix for the affected diagrams. Switch to the bot's suggested regex (signed numbers, single or double quotes, comma or space separators) and reject NaN parts. Adds Vitest coverage for signed origins, single quotes, comma separators, the malformed/no-viewBox null paths, and an end-to-end normalize test that fails against the old regex. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
- None.
Questions
- None.
Summary
Review mode: follow-up after new commits
No high-confidence issues found in the current diff. The prior bot finding around signed/negative SVG viewBox values is addressed in the new commit, including focused coverage for getMermaidSvgLayoutSize and normalization.
Residual risk: I could not run the Bun-backed web typecheck or tests in this runner because bun is unavailable.
Testing
- Not run (automation):
bun run typecheck:webfailed becausebunis not installed. - Not run (automation):
bun run test:web -- src/components/ZoomableLightbox.test.ts src/components/assistant-ui/mermaid-diagram.test.tsx src/components/assistant-ui/mermaid-svg-id.test.tsfailed becausebunis not installed. git diff --checkpassed.
HAPI Bot
Summary
Problem
Mermaid renders inline in chat (#567) but dense diagrams are hard to read on mobile with horizontal scroll only. Images already have a lightbox (#715, polish in #723).
Approach
ZoomableLightboxcomponent (same interaction model asImagePreview, including backdrop dismiss).MermaidDiagramwraps successful renders in a zoom-in trigger and opens the lightbox with a freshmermaid.render()call.Testing
cd web && bun run test src/components/assistant-ui/mermaid-diagram.test.tsxcd web && bun run test src/components/assistant-ui/mermaid-diagram.live.test.tsxCloses #737.
Made with Cursor